<?php
// +----------------------------------------------------------------------
// | ZengCMS [ 火火 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2018 http://zengcms.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 火火 <zengcms@qq.com>
// +----------------------------------------------------------------------

// +----------------------------------------------------------------------
// | DataHandle数据处理扩展类-使用trait特性
// +----------------------------------------------------------------------
namespace app\api\lib;

use think\facade\Env;
use think\facade\Cache;
use app\api\lib\OpensslAES;
use app\api\lib\exception\HttpException;
use app\api\lib\exception\MissException;

trait DataHandle
{
    ##############################################第一种签名校验方式 开始############################################
    // 检查签名
    protected function checkSign()
    {
        // 判断时间戳是否正确
        $timestamp = $this->request->get('timestamp');
        if(!$timestamp || intval($timestamp) <= 1 || strlen($timestamp) !== 13){
            throw new HttpException(19997, '时间戳错误！', 400);
        }
        // 判断时间戳是否在多少秒之内
        if (abs(time() - ceil(intval($timestamp) / 1000)) > config('setting.app_signature_time')) {
            throw new HttpException(19997, '请求超时！', 400);
        }
        // 判断签名是否存在
        if ($this->request->header('signature') == null) {
            throw new HttpException(19997, '参数错误！', 400);
        }
        // 生成签名
        $signature = $this->createSignature();
        // 检查是否重复提交
        $result = Cache::store('file')->get('signature_' . $signature);
        if($result){
            throw new HttpException(19997, '请勿重复请求！', 400);
        }
        // 验证签名
        if ($this->request->header('signature') != $signature) {
            throw new HttpException(19997, '签名错误！', 400);
        }
        Cache::store('file')->set('signature_' . $signature, 1, config('setting.app_signature_cache_time'));
    }
    // 后端生成签名
    protected function createSignature()
    {
        $signature_data = $this->request->except(['ver'], 'param');//排除版本ver参数
        ksort($signature_data);//根据键名排序
        $signature_data_str = http_build_query($signature_data);
        return md5($signature_data_str.config('setting.app_signature_salt'));
    }
    ##############################################第一种签名校验方式 结束############################################
    ##############################################第二种签名校验方式 开始############################################
    // 检查签名
    protected function checkSignature()
    {
        // 获取头部信息headers
        $headers = request()->header();
        // 判断headers里的签名signature参数是否合法
        if (!isset($headers['signature']) || empty($headers['signature'])) {
            throw new MissException([
                // 异常代码
                'code'=>0,
                // 异常消息内容
                'message' => 'headers里的签名signature参数有误！',
                // http状态码
                'httpCode' => 404
            ]);
        }
        // 判断headers里的时间戳timestamp参数是否合法
        if (!isset($headers['timestamp']) || empty($headers['timestamp']) || intval($headers['timestamp']) <= 1 || strlen($headers['timestamp']) !== 13) {
            throw new MissException([
                // 异常消息内容
                'message' => 'headers里的时间戳timestamp参数有误！',
                // http状态码
                'httpCode' => 404
            ]);
        }
        // 判断随机nonce参数是否存在
        if (!isset($headers['nonce']) || empty($headers['nonce'])) {
            throw new MissException([
                // 异常消息内容
                'message' => 'headers里的随机数nonce参数有误！',
                // http状态码
                'httpCode' => 404
            ]);
        }
        // 检查signature是否正常 注意时间戳是十三位的，而php time()是十位的
        if (!self::checkSignaturePass($headers)) {
            throw new MissException([
                // 异常消息内容
                'message' => '授权码signature失败！',
                // http状态码
                'httpCode' => 401
            ]);
        }
        // 缓存签名signature(文件)
        // 1、文件(单台服务器)  2、mysql 3、redis (mysql、redis多台服务器用和分布式session失效机制一样道理)
        // 值都为1
        Cache::store('file')->set($headers['signature'],1,config('setting.app_signature_cache_time'));
    }
    /**
     * 检查signature是否正常合法
     * @param  $headers [description]
     * @return          [description]
     */
    public static function checkSignaturePass($headers)
    {
        // 对签名sign进行OpensslAES解密
        $str = (new OpensslAES())->decrypt($headers['signature']);
        // 判断签名sign是否合法
        if (empty($str)) {
            throw new MissException([
                //异常消息内容
                'message' => '签名不合法！',
                // http状态码
                'httpCode' => 401
            ]);
        }
        // 字符串signature=xx&timestamp=xxx&nonce=xxx&xxx=xxx转为数组
        parse_str($str, $arr);
        // 判断是否是数组
        if (!is_array($arr)) {
            throw new MissException([
                // 异常消息内容
                'message' => 'headers里的签名signature参数不合法！',
                // http状态码
                'httpCode' => 400
            ]);
        }
        // 检查时间戳是否一致
        if (!isset($arr['timestamp']) || empty($arr['timestamp']) || $arr['timestamp'] != $headers['timestamp']) {
            return false;
        }
        // 检查随机字符串是否一致
        if (!isset($arr['nonce']) || empty($arr['nonce']) || $arr['nonce'] != $headers['nonce']) {
            return false;
        }
        // 项目上线时做判断，签名是否超时和重复使用
        if (Env::get('APP_DEBUG',false) == false) {
            // 判断请求是否超过规定时间 注意$arr['time']是13位时间戳，而time()是10位的时间戳
            if (abs(time() - ceil($arr['timestamp'] / 1000)) > config('setting.app_signature_time')) {
                throw new MissException([
                    // 异常消息内容
                    'message' => '请求超时！',
                    // http状态码
                    'httpCode' => 401
                ]);
            }
            // 唯一性判定 (判断标签signature是否已经请求了 timestamp和nonce是不断变化的,那么signature也是不断变化的)
            if(Cache::store('file')->get($headers['signature'])){
                throw new MissException([
                    // 异常消息内容
                    'message' => '请勿重复请求！',
                    // http状态码
                    'httpCode' => 401
                ]);
            }
        }
        return true;
    }
    ##############################################第二种签名校验方式 结束############################################
    // 检查API接口请求频率
    public function checkFrequency()
    {
        // 限制次数为100
        $limit = 100;
        // 限制时间为60秒
        $time = 60;
        $url = request()->url();
        $api = 'api_'.get_client_ip().'_'.parse_url($url)['path'];
        $result = Cache::store('file')->get($api);
        if($result){
            if ($result >= $limit) {
                throw new MissException([
                    // 异常消息内容
                    'message' => 'your have too many request',
                    // http状态码
                    'httpCode' => 400
                ]);
            }
            // 键值递增
            Cache::store('file')->inc($api);
        }else{
            Cache::store('file')->set($api,1,$time);
        }
    }
    /**
     * 封装统一返回数据格式
     * @param  array   $data     [数据]
     * @param  integer $code     [异常代码]
     * @param  string  $message  [异常消息内容]
     * @param  integer $httpCode [http状态码]
     * @param  string  $dtype    [返回数据格式：json或xml或jsonp，默认json]
     * @param  array   $header   [头部]
     * @param  array   $options  [参数]
     * @return [type]            [description]
     */
    public function return_data($data = [], $code = 0, $message = 'success', $httpCode = 200, $dtype = 'json', $header = [], $options = [])
    {
        $return_data = [
            // 异常代码
            'code' => $code,
            // 异常消息内容
            'message' => $message,
            // 数据
            'data' => $data
        ];
        switch ($dtype) {
            case 'json':
                return json($return_data, $httpCode, $header, $options);
                break;
            case 'xml':
                return xml($return_data, $httpCode, $header, $options);
                break;
            case 'jsonp':
                return jsonp($return_data, $httpCode, $header, $options);
                break;
            default:
                return json($return_data, $httpCode, $header, $options);
                break;
        }
    }
    /**
     * 返回API数据
     * @param  array   $data     [返回的数据]
     * @param  integer $httpCode [http状态码]
     * @param  string  $dtype    [返回数据格式：json或xml或jsonp，默认json]
     * @param  array   $header   [头部]
     * @param  array   $options  [参数]
     * @return [type]            [description]
     */
    public function returnData($data = [], $httpCode = 200, $dtype = 'json', $header = [], $options = [])
    {
        switch ($dtype) {
            case 'json':
                return json($data, $httpCode, $header, $options);
                break;
            case 'xml':
                return xml($data, $httpCode, $header, $options);
                break;
            case 'jsonp':
                return jsonp($data, $httpCode, $header, $options);
                break;
            default:
                return json($data, $httpCode, $header, $options);
                break;
        }
    }
}
